/*
 * @Author: zi.yang
 * @Date: 2022-11-27 00:43:38
 * @LastEditors: zi.yang
 * @LastEditTime: 2022-12-04 21:57:18
 * @Description: index
 * @FilePath: /promise/lib/index.ts
 */
/* eslint-disable import/no-import-module-exports */
import {
  convertFunc,
  everyExists,
  getFulfilledFunc,
  getRejectedFunc,
  isFunction,
  isIterable,
  isPromise,
  thenableCallbackAdapter,
} from './utils/common';
import createPromiseInstance from './utils/createPromiseInstance';
import { throwAggregateError, throwIsNotIterable, throwTypeError } from './utils/throwError';

/**
 * Promise
 */
class ToyPromise<T> implements ToyPromiseInterface<T> {
  // 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
  status: RunStatus = RunStatus.PENDING;

  // 1.3 “value” is any legal JavaScript value (including undefined, a thenable, or a promise).
  value: ResolveValueType<T>;

  // 1.5 “reason” is a value that indicates why a promise was rejected.
  reason: ResolveValueType<T>;

  // resolve async callback queue
  onResolvedCallbacks: Array<Function> = [];

  // reject async callback queue
  onRejectedCallbacks: Array<Function> = [];

  constructor(executor: ToyPromiseExecutor) {
    if (!(this instanceof ToyPromise)) {
      throw throwTypeError('Promises must be constructed via new');
    }

    // ES262 - 25.4.3.1 Promise ( executor ) throws TypeError if 'executor' is not Callable:
    if (!isFunction(executor)) {
      throw throwTypeError('Promise resolver is not a function');
    }

    // 1.4 “exception” is a value that is thrown using the throw statement.
    try {
      executor(this.resolve, this.reject);
    } catch (error) {
      this.reject(error);
    }
  }

  // resolve callback
  resolve = (value: ResolveValueType<T>): void => {
    if (this.status !== RunStatus.PENDING) return;
    this.value = value;
    this.status = RunStatus.FULFILLED;
    this.onResolvedCallbacks.forEach((cb: Function) => cb());
    this.onResolvedCallbacks.length = 0;
  };

  // reject callback
  reject = (reason: RejectReasonType<T>): void => {
    if (this.status !== RunStatus.PENDING) return;
    this.reason = reason;
    this.status = RunStatus.REJECTED;
    this.onRejectedCallbacks.forEach((cb: Function) => cb());
    this.onRejectedCallbacks.length = 0;
  };

  // 2.2 The `then` Method
  // - A promise must provide a then method to access its current or eventual value or reason.
  // - A promise’s then method accepts two arguments: onFulfilled 、 onRejected
  then<F, R>(
    onFulfilled?: (value: T) => F | ToyPromiseInterface<F>,
    onRejected?: (reason: T) => R | ToyPromiseInterface<R>,
  ): ToyPromiseInterface<F | R> {
    // 2.2.1 Both onFulfilled and onRejected are optional arguments:
    // - If onFulfilled is not a function, it must be ignored.
    const onFulfilledCallback = getFulfilledFunc(onFulfilled);
    // - If onRejected is not a function, it must be ignored.
    const onRejectedCallback = getRejectedFunc(onRejected);

    // ES262-25.4.1.6.1 step 8 invalid 'resolve','reject':  throws TypeError with Promise.then:
    const promiseInstance = createPromiseInstance(<ToyPromiseConstructor>this.constructor);

    // 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
    switch (this.status) {
      case RunStatus.FULFILLED:
        thenableCallbackAdapter(promiseInstance.Instance, this.value, onFulfilledCallback);
        break;

      case RunStatus.REJECTED:
        thenableCallbackAdapter(promiseInstance.Instance, this.reason, onRejectedCallback);
        break;

      // 如果调用 then 时，状态仍为 pending 则说明是异步的
      case RunStatus.PENDING:
        this.onResolvedCallbacks.push(() => {
          thenableCallbackAdapter(promiseInstance.Instance, this.value, onFulfilledCallback);
        });
        this.onRejectedCallbacks.push(() => {
          thenableCallbackAdapter(promiseInstance.Instance, this.reason, onRejectedCallback);
        });
        break;

      // 预期之外的状态, 理论上不会出现
      default:
        break;
    }

    // 2.2.7 `then` must return a promise [3.3].
    return promiseInstance.Instance;
  }

  // catch 方法
  // 1. 该方法优先级低于 callback error
  // 2. 该方法与 callback 同时出现时，只会执行 callback error
  catch(onRejected?: PromiseRejectFunction): ToyPromiseInterface<RejectReasonType<T>> {
    return this.then(null, getRejectedFunc(onRejected));
  }

  // finally 方法
  finally(onFinally: Function): ToyPromiseInterface<T> {
    const onFinallyCallback = convertFunc(onFinally);
    return this.then(
      (data: any) => {
        return ToyPromise.resolve(onFinallyCallback()).then(() => data);
      },
      (error: any) => {
        return ToyPromise.reject(onFinallyCallback()).then(() => {
          throw error;
        });
      },
    );
  }

  // Promise.resolve
  static resolve<T>(value: ToyPromiseInterface<T> | T): ToyPromiseInterface<T> {
    // ES262-25.4.4.5 Promise.resolve( x )
    // passes through an unsettled promise created with the same constructor as 'this':
    if (isPromise(value)) return value as ToyPromiseInterface<T>;
    const promiseInstance = createPromiseInstance(this);
    promiseInstance.resolveHook(value);
    return promiseInstance.Instance;
  }

  // Promise.reject
  static reject<T>(reason: T): ToyPromiseInterface<T> {
    const promiseInstance = createPromiseInstance(this);
    promiseInstance.rejectHook(reason);
    return promiseInstance.Instance;
  }

  /**
   * Promise.all
   * @param iterable
   * @returns
   */
  // TODO: Map、TypedArray、Map 和 Set 支持
  static all<T>(iterable: Iterable<T | ToyPromiseInterface<T>>): ToyPromiseInterface<T> {
    const promiseInstance = createPromiseInstance(this);

    // ES262-25.4.4.1 Promise.all( iterable ) should immediately reject for non-iterable argument:
    if (!isIterable(iterable)) {
      promiseInstance.rejectHook(throwIsNotIterable('all'));
      return promiseInstance.Instance;
    }

    if (typeof iterable === 'string') {
      promiseInstance.resolveHook((<string>iterable).split(''));
      return promiseInstance.Instance;
    }

    if (Array.isArray(iterable)) {
      if (iterable.length === 0) {
        promiseInstance.resolveHook([]);
      }

      const result: Array<T> = [];
      let index = 0;

      // 进度队列：保证数据按序返回
      const processData = (idx: number, data: T) => {
        return () => {
          result[idx] = data;
          index += 1;
          if (index === iterable.length) {
            promiseInstance.resolveHook(result);
          }
        };
      };

      iterable.forEach((value: T | ToyPromiseInterface<T>, idx: number) => {
        if (isPromise(value)) {
          (value as ToyPromiseInterface<T>).then((res: T) => {
            processData(idx, res);
          }, promiseInstance.rejectHook);
        } else {
          processData(idx, value as T);
        }
      });
    }
    return promiseInstance.Instance;
  }

  // TODO: Map、TypedArray、Map 和 Set 支持
  static race<T>(iterable: Iterable<T | ToyPromiseInterface<T>>): ToyPromiseInterface<T> {
    const promiseInstance = createPromiseInstance(this);

    if (!isIterable(iterable)) {
      promiseInstance.rejectHook(throwIsNotIterable('race'));
      return promiseInstance.Instance;
    }

    if (typeof iterable === 'string') {
      promiseInstance.rejectHook((<String>iterable).split(''));
      return promiseInstance.Instance;
    }

    if (Array.isArray(iterable)) {
      // ES262-25.4.4.3 Promise.race with 0-element array should return a pending promise
      if (iterable.length === 0) {
        return promiseInstance.Instance;
      }

      // start iteration for each item
      iterable.forEach((value: T | ToyPromiseInterface<T>) => {
        ToyPromise.resolve(value).then(promiseInstance.resolveHook, promiseInstance.rejectHook);
      });
    }
    return promiseInstance.Instance;
  }

  static allSettled<T>(iterable: Iterable<T | ToyPromiseInterface<T>>): ToyPromiseInterface<T> {
    const promiseInstance = createPromiseInstance(this);
    if (!isIterable(iterable)) {
      promiseInstance.rejectHook(throwIsNotIterable('allSettled'));
      return promiseInstance.Instance;
    }

    if (Array.isArray(iterable)) {
      if (iterable.length === 0) {
        promiseInstance.resolveHook([]);
        return promiseInstance.Instance;
      }
      type DataType = {
        status: RunStatus;
        value?: any;
        reason?: any;
      };
      const result: DataType[] = [];
      const processData = (idx: number, status: RunStatus, value: any) => {
        if (status === RunStatus.FULFILLED) {
          result[idx] = { status, value };
        } else if (status === RunStatus.REJECTED) {
          result[idx] = { status, reason: value };
        }
        if (everyExists(result, iterable.length)) {
          promiseInstance.resolveHook(result);
        }
      };
      iterable.forEach((item: any, idx: number) => {
        ToyPromise.resolve(item).then(
          (value: any) => {
            processData(idx, RunStatus.FULFILLED, value);
          },
          (reason: any) => {
            processData(idx, RunStatus.REJECTED, reason);
          },
        );
      });
    }
    return promiseInstance.Instance;
  }

  static any<T>(iterable: Iterable<T | ToyPromiseInterface<T>>): ToyPromiseInterface<T> {
    const promiseInstance = createPromiseInstance(this);
    if (!isIterable(iterable)) {
      promiseInstance.rejectHook(throwIsNotIterable('any'));
      return promiseInstance.Instance;
    }

    if (Array.isArray(iterable)) {
      if (iterable.length === 0) {
        promiseInstance.rejectHook(throwAggregateError());
        return promiseInstance.Instance;
      }
      const processReject = () => {
        let rejectCounter = 0;
        return () => {
          rejectCounter += 1;
          if (rejectCounter === iterable.length) {
            promiseInstance.rejectHook(throwAggregateError());
          }
        };
      };
      iterable.forEach((item: ToyPromiseInterface<T> | T) =>
        ToyPromise.resolve(item).then(promiseInstance.resolveHook, processReject()),
      );
    }
    return promiseInstance.Instance;
  }
}

export default ToyPromise;
